/*
 * Graphs
 * Version: 0.1
 * Date: 2008-10-25
 * Author: gp08-nhn
 */

package keyboardhero;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.net.URL;
import java.util.*;
import keyboardhero.Util.*;
import keyboardhero.Game.*;
import keyboardhero.MidiSequencer.*;

final class Graphs {
	
	//-------------------- NESTED CLASSES --------------------
	
	final static class ScoreMessage implements Comparable<ScoreMessage> {
		
		//********** VARIABLES **********
		
		// ××××× SETTINGS VARIBALES ×××××
		static final int TIME_LIMIT = 3000;
		private static final int MARGIN = 10;
		private static final int UP_MOVEMENT = 50;
		static final String FONT_NAME = "Arial Black";
		static final int FONT_STYLE = Font.BOLD;
		static final int FONT_SIZE = 16;
		private static final String IMAGEDIR = "scoreimages/";
		private static final Integer SCALE_HINT = Image.SCALE_SMOOTH;
		
		// ××××× CACHE VARIBALES ×××××
		private static final Font FONT = new Font(FONT_NAME, FONT_STYLE, FONT_SIZE);
		private static final FontRenderContext FONT_RENDER_CONTEXT = new FontRenderContext(new AffineTransform(), true, false);
		static final TreeSet<ScoreMessage> MESSAGES = new TreeSet<ScoreMessage>();
		private static final int HEIGHT = (int)FONT.getMaxCharBounds(FONT_RENDER_CONTEXT).getHeight() + MARGIN;
		static final Image WRONG = Util.TOOLKIT.getImage(Util.getURL(IMAGEDIR + "wrong.gif"));
		static final Image POOR = Util.TOOLKIT.getImage(Util.getURL(IMAGEDIR + "poor.gif"));
		static final Image GOOD = Util.TOOLKIT.getImage(Util.getURL(IMAGEDIR + "good.gif"));
		static final Image GREAT = Util.TOOLKIT.getImage(Util.getURL(IMAGEDIR + "great.gif"));
		static final Image PERFECT = Util.TOOLKIT.getImage(Util.getURL(IMAGEDIR + "perfect.gif"));
		
		// Obj<Image> is used instead of Images, because then the same method can be used to change them -> Util.resizeImage (resized just passes the values)
		// it's bit like reference passing (& sign in C)
		private static Obj<Image> imgWrong, imgPoor, imgGood, imgGreat, imgPerfect;
		private static String wrong, poor, good, great, perfect;
		private static int wrongw, poorw, goodw, greatw, perfectw;
		
		// ××××× INSTANCE VARIBALES ×××××
		final Color color;
		final int score;
		private final byte message;
		final long time;
		private int y, width;
		private final int x, startY;
		final Key key;
		
		//********** CORE FUNCTIONS **********
		
		static { // image loading is asynchronous -> precaching
			if(!Util.preloadIfToolkit(WRONG, Util.IMAGE_OBSERVER)) {
				WRONG.getWidth(Util.IMAGE_OBSERVER);
			}
			if(!Util.preloadIfToolkit(POOR, Util.IMAGE_OBSERVER)) {
				WRONG.getWidth(Util.IMAGE_OBSERVER);
			}
			if(!Util.preloadIfToolkit(GOOD, Util.IMAGE_OBSERVER)) {
				WRONG.getWidth(Util.IMAGE_OBSERVER);
			}
			if(!Util.preloadIfToolkit(GREAT, Util.IMAGE_OBSERVER)) {
				WRONG.getWidth(Util.IMAGE_OBSERVER);
			}
			if(!Util.preloadIfToolkit(PERFECT, Util.IMAGE_OBSERVER)) {
				WRONG.getWidth(Util.IMAGE_OBSERVER);
			}
			
			imgWrong = new Obj<Image>(WRONG);
			imgPoor = new Obj<Image>(POOR);
			imgGood = new Obj<Image>(GOOD);
			imgGreat = new Obj<Image>(GREAT);
			imgPerfect = new Obj<Image>(PERFECT);
		}
		
		ScoreMessage(Key key, int score) {
			this(key, score, System.currentTimeMillis());
		}
		
		@SuppressWarnings("null")
		ScoreMessage(Key key, int score, long time) {
			if(score < 0) {
				message = -1;
				width = wrongw;
				color = getColor("msgWrong");
			} else {
				color = Util.getFadingColor((double)score / 100, Graphs.getColor("msgPoor"), Graphs.getColor("msgPerfect"));
				
				if(score < 40) {
					message = 0;
					width = poorw;
				} else if(score < 65) {
					message = 1;
					width = goodw;
				} else if(score < 90) {
					message = 2;
					width = greatw;
				} else {
					message = 3;
					width = perfectw;
				}
			}
			this.score = score;
			this.time = time;
			this.key = key;
			
			final int keyX = canvas.getKeyX(key);
			if(keyX - width / 2 < MARGIN) this.x = MARGIN;
			else if(keyX + width / 2 + MARGIN > Graphs.WIDTH) this.x = Graphs.WIDTH - width - MARGIN;
			else x = keyX - width / 2;
			
			LinkedList<ScoreMessage> clashings = new LinkedList<ScoreMessage>();
			synchronized (MESSAGES) {
				for(ScoreMessage message : MESSAGES) {
					if(isClashing(x, width, message)) {
						clashings.add(message);
					}
				}
				
				int minY = canvas.getMiddleTop() + MARGIN + UP_MOVEMENT;
				int y = minY;
				
				Collections.sort(clashings, new Comparator<ScoreMessage>() {
					public final int compare(ScoreMessage o1, ScoreMessage o2) {
						if(o1.y < o2.y) {
							return -1;
						} else if(o1.y == o2.y) {
							return 0;
						} else {
							return 1;
						}
					}
				});
				
				Iterator<ScoreMessage> iterator = clashings.iterator();
				
				while(true) {
					if(iterator.hasNext()) {
						int nextY = iterator.next().y;
						if(nextY - y >= HEIGHT) {
							break;
						} else {
							y = nextY + HEIGHT;
						}
					} else {
						break;
					}
				}
				
				if(y + MARGIN > canvas.getMiddleBottom()) {
					int minClashingNum = Integer.MAX_VALUE;
					long minTime = Long.MAX_VALUE;
					ScoreMessage minMessage = null;
					LinkedList<ScoreMessage> minAllClashings = new LinkedList<ScoreMessage>();
					
					for(ScoreMessage message : clashings) {
						LinkedList<ScoreMessage> allClashings = new LinkedList<ScoreMessage>();
						for(ScoreMessage m2 : clashings) {
							if(isClashing(x, message.y, width, m2)) {
								allClashings.add(m2);
							}
						}
						
						if(message.y >= minY) {
							final int clashingNum = allClashings.size();
							if(clashingNum < minClashingNum) {
								minClashingNum = clashingNum;
								minMessage = message;
								minTime = message.time;
								minAllClashings = allClashings;
							} else if(clashingNum == minClashingNum && message.time < minTime) {
								minClashingNum = clashingNum;
								minMessage = message;
								minTime = message.time;
								minAllClashings = allClashings;
							}
						}
					}
					
					this.y = minMessage.y;
					for(ScoreMessage message : minAllClashings) {
						MESSAGES.remove(message);
					}
				} else {
					this.y = y;
				}
				startY = this.y;
				
				MESSAGES.add(this);
			}
		}
		
		private static boolean isClashing(int x, int width, ScoreMessage m) {
			return ((x <= m.x && m.x <= x + width) || (m.x <= x && x <= m.x + m.width));
		}
		
		private static boolean isClashing(int x, int y, int width, ScoreMessage m) {
			return ((x <= m.x && m.x <= x + width) || (m.x <= x && x <= m.x + m.width)) && ((y <= m.y && m.y <= y + HEIGHT) || (m.y <= y && y <= m.y + HEIGHT));
		}
		
		//********** SETTER / GETTER FUNCTIONS **********
		
		final String getMessage() {
			switch(message) {
				case -1:
					return wrong;
				case 0:
					return poor;
				case 1:
					return good;
				case 2:
					return great;
				default: // case 3:
					return perfect;
			}
		}
		
		final int getX(Font font) {
			width = getStringWidth(font, getMessage());
			
			try {
				final int keyX = canvas.getScaledKeyX(key);
				int cache;
				
				if(keyX - width / 2 < MARGIN) return MARGIN;
				else if(keyX + width / 2 + MARGIN > (cache = Graphs.getCanvas().getWidth())) return cache - width - MARGIN;
				else return keyX - width / 2;
			} finally {
				width += MARGIN;
			}
		}
		
		final int getY() {
			y = startY - (int)(UP_MOVEMENT * (System.currentTimeMillis() - time) / TIME_LIMIT);
			return y;
		}
		
		final Image getImage() {
			switch(message) {
				case -1:
					return imgWrong.getObj();
				case 0:
					return imgPoor.getObj();
				case 1:
					return imgGood.getObj();
				case 2:
					return imgGreat.getObj();
				default: // case 3:
					return imgPerfect.getObj();
			}
		}
		
		//********** EVENT FUNCTIONS **********
		
		static void resized(double ratio) {
			Util.resizeImage(ratio, WRONG, imgWrong, SCALE_HINT);
			Util.resizeImage(ratio, POOR, imgPoor, SCALE_HINT);
			Util.resizeImage(ratio, GOOD, imgGood, SCALE_HINT);
			Util.resizeImage(ratio, GREAT, imgGreat, SCALE_HINT);
			Util.resizeImage(ratio, PERFECT, imgPerfect, SCALE_HINT);
		}
		
		static void updateTexts() {
			wrong = Util.getMsg("WRONG");
			poor = Util.getMsg("POOR");
			good = Util.getMsg("GOOD");
			great = Util.getMsg("GREAT");
			perfect = Util.getMsg("PERFECT");
			
			wrongw = getStringWidth(wrong) + MARGIN;
			poorw = getStringWidth(poor) + MARGIN;
			goodw = getStringWidth(good) + MARGIN;
			greatw = getStringWidth(great) + MARGIN;
			perfectw = getStringWidth(perfect) + MARGIN;
		}
		
		static {
			updateTexts();
		}
		
		//********** UTILITY FUNCTIONS **********
		
		private static final int getStringWidth(String str) {
			return (int)FONT.getStringBounds(str, FONT_RENDER_CONTEXT).getWidth();
		}
		
		private static final int getStringWidth(Font font, String str) {
			return (int)font.getStringBounds(str, FONT_RENDER_CONTEXT).getWidth();
		}
		
		//********** OVERRIDING FUNCTIONS **********
		
		public boolean equals(Object o) {
			if(o instanceof ScoreMessage) {
				ScoreMessage m = (ScoreMessage)o;
				return (time == m.time && x == m.x && y == m.y && score == m.score && message == m.message);
			}
			return false;
		}
		
		public int compareTo(ScoreMessage o) {
			if(time < o.time) {
				return -1;
			} else if(time == o.time) {
				if(x < o.x) {
					return 1;
				} else if(x == o.x) {
					if(y < o.y) {
						return 1;
					} else if(y == o.y) {
						if(score < o.score) {
							return 1;
						} else if(score == o.score) {
							if(message < o.message) {
								return 1;
							} else if(message == o.message) {
								return 0;
							} else {
								return -1;
							}
						} else {
							return -1;
						}
					} else {
						return -1;
					}
				} else {
					return -1;
				}
			} else {
				return 1;
			}
		}
	}
	
	//-------------------- VARIABLES --------------------
	
	// ××××× SETTINGS VARIABLES ×××××
	
	/** The background color of the game. */
	final static Color BG_COLOR = Color.WHITE;
	
	final static int WIDTH = 640;
	final static int HEIGHT = 480;
	
	final static double WHRATIO = (double)WIDTH / HEIGHT;
	
	final static String[] RENDERERS = new String[] {"Swing"};
	
	static final int SCALE_HINT = Image.SCALE_SMOOTH;
	static final String THEME_DIR = "themes/";
	static final String DEFAULT_THEME = "Soft Blue";
	private static String themeDir = DEFAULT_THEME+"/";
	private static URL bgMain;
	
	/** The HashMap for colors. */
	private final static HashMap<String, Color> COLORS = new HashMap<String, Color>();
	static {
		COLORS.put("front", Color.WHITE);
		COLORS.put("frontShadow", Color.BLACK);
		COLORS.put("error", Color.RED);
		COLORS.put("success", new Color(0, 128, 0));
		COLORS.put("toplistNewEntry", Color.RED);
		COLORS.put("topText", Color.WHITE);
		COLORS.put("note", Color.CYAN);
		COLORS.put("noteBorder", Color.BLACK);
		COLORS.put("maxVelocity", Color.DARK_GRAY);
		COLORS.put("kbBlack", Color.BLACK);
		COLORS.put("kbWhite", Color.WHITE);
		COLORS.put("kbFontActive", Color.BLACK);
		COLORS.put("kbFontOnWhite", Color.BLACK);
		COLORS.put("kbFontOnBlack", Color.WHITE);
		COLORS.put("msgWrong", new Color(128, 0, 0));
		COLORS.put("msgPoor", Color.BLUE);
		COLORS.put("msgPerfect", Color.GREEN);
		COLORS.put("sidePanelBg", new Color(230, 230, 230));
	}
	
	// ××××× CORE VARIABLES ×××××
	
	private static GraphRenderer canvas = null;
	
	// ××××× CACHE VARIABLES ×××××
	
	private static int lastNoteIndex;
	private static int noteLetters = Util.getPropInt("viewGenNoteLetters");
	private static boolean keyboard = Util.getPropBool("viewGenDisplayKeyboard");
	private static int keyboardLetters = Util.getPropInt("viewGenKeyboardLetters");
	private static int firstKeyInMidi = Util.getPropInt("viewGenFirstKey");
	private static int firstKey = Key.toKey(firstKeyInMidi);
	private static int lastKeyInMidi = Util.getPropInt("viewGenLastKey");
	private static int lastKey = Key.toKey(lastKeyInMidi);
	private static boolean scoreImages = Util.getPropBool("viewGenScoreImages");
	
	//-------------------- CORE FUNCTIONS --------------------
	
	static Component initialize(int type) {
		try {
			switch(type) {
				/*case 1 :
					canvas = new RendererOpenGL();
					break;*/
				default:
					type = 0;
					canvas = new RendererSwing();
					break;
			}
		} catch(Throwable e) {
			Util.error(Util.getMsg("Err_RendererInit") + RENDERERS[type], e.toString());
			if(canvas == null) {
				switch(type) {
					case 1:
						canvas = new RendererSwing();
						break;
					/*default :
						canvas = new RendererOpenGL();
						break;*/
				}
			}
			
			if(canvas instanceof RendererSwing) Util.setProp("renderer", 0);
			//else if (canvas instanceof RendererOpenGL) Util.setProp("renderer", 1);
		}
		
		return Graphs.getCanvas();
	} // end method
	
	/**
	 * Method calling all the displaying methods and drawing all that should appear on the screen.
	 * It draws the top part, the keyboard, the score messages, the "Paused" message if the game is
	 * paused or the notes if the game is active.
	 * 
	 * @param g
	 *            the Graphics context.
	 */
	static void draw(Graphics g) {
		final State state = Game.getState();
		
		canvas.drawBackground(g);
		
		if(Game.isPaused()) {
			canvas.drawTop(g, state);
			canvas.drawKeyboard(g, state.activeKeys);
			canvas.drawScoreMessages(g, ScoreMessage.MESSAGES);
			canvas.drawPausedScreen(g);
		} else {
			final Note[] notes = state.notes;
			
			final int max = notes.length;
			boolean firstRight = true;
			
			for(int i = lastNoteIndex; i < max; ++i) {
				Note note = notes[i];
				if(state.time <= note.time + note.length) {
					if(firstRight) {
						firstRight = false;
						lastNoteIndex = i;
					}
					
					if(note.time <= state.time + state.fallingTime) {
						if(firstKey <= note.key.key && (note.key.key < lastKey || (note.key.key == lastKey && !note.key.higher))) {
							canvas.drawNote(g, state, note);
						}
					} else {
						break;
					}
				}
			}
			
			canvas.drawTop(g, state);
			canvas.drawKeyboard(g, state.activeKeys);
			canvas.drawScoreMessages(g, ScoreMessage.MESSAGES);
		}
		
	} // end method
	
	//-------------------- EVENT FUNCTIONS --------------------
	
	/** Updates all the text objects' cached string. */
	static void updateTexts() {
		ScoreMessage.updateTexts();
		canvas.updateTexts();
	}
	
	static void updateTheme() {
		final String bgDir = THEME_DIR + themeDir;
		bgMain = Util.getURL(bgDir + "bgMain.jpg");
		if(canvas != null) canvas.updateTheme();
	}
	
	static void gameStarted() {
		canvas.gameStarted();
	}
	
	static void gameEnded() {
		canvas.gameEnded();
	}
	
	static void gamePaused() {
		canvas.gamePaused();
	}
	
	static void gameResumed() {
		canvas.gameResumed();
	}
	
	static void fullScreenStateChanged(boolean fullScreen) {
		canvas.fullScreenStateChanged(fullScreen);
	}
	
	static void activateKey(MidiSequencer.Key k) {
		canvas.activateKey(k);
	}
	
	static void deactivateKey(MidiSequencer.Key k) {
		canvas.deactivateKey(k);
	}
	
	static void notesReseted() {
		lastNoteIndex = 0;
	}
	
	static void keysChanged() {
		canvas.keysChanged();
		canvas.repaint();
	}
	
	static void keyboardStateChanged() {
		canvas.keyboardStateChanged();
		canvas.repaint();
	}
	
	static void lettersChanged() {
		canvas.lettersChanged();
		canvas.repaint();
	}
	
	static void setFullscreenChangeAllowed(boolean allowed) {
		canvas.setFullscreenChangeAllowed(allowed);
	}
	
	static void closure() {
		Util.setProp("viewGenNoteLetters", noteLetters);
		Util.setProp("viewGenDisplayKeyboard", keyboard);
		Util.setProp("viewGenKeyboardLetters", keyboardLetters);
		Util.setProp("viewGenFirstKey", firstKeyInMidi);
		Util.setProp("viewGenLastKey", lastKeyInMidi);
		Util.setProp("viewGenScoreImages", scoreImages);
	}
	
	//-------------------- SETTER / GETTER FUNCTIONS --------------------
	/**
	 * Getter method for returning the canvas field as a Component.
	 * 
	 * @return the {@link #canvas} casted to a Component.
	 */
	final static Component getCanvas() {
		return (Component)canvas;
	} // end method
	
	final static Color getColor(String key) {
		return COLORS.get(key);
	}
	
	final static void setTheme(String theme) {
		themeDir = theme + '/';
		updateTheme();
	}
	
	static final URL getBgMain() {
		return bgMain;
	}
	
	static final int getNoteLetters() {
		return noteLetters;
	}
	
	static final void setNoteLetters(int noteLetters) {
		Graphs.noteLetters = noteLetters;
		lettersChanged();
	}
	
	static final int getKeyboardLetters() {
		return keyboardLetters;
	}
	
	static final void setKeyboardLetters(int keyboardLetters) {
		Graphs.keyboardLetters = keyboardLetters;
		lettersChanged();
	}
	
	static final int getFirstKeyInMidi() {
		return firstKeyInMidi;
	}
	
	static final void setFirstKey(int firstKeyInMidi) {
		if(Graphs.firstKeyInMidi != firstKeyInMidi) {
			Graphs.firstKeyInMidi = firstKeyInMidi;
			firstKey = Key.toKey(firstKeyInMidi);
			keysChanged();
		}
	}
	
	static final int getLastKeyInMidi() {
		return lastKeyInMidi;
	}
	
	static final void setLastKey(int lastKeyInMidi) {
		if(Graphs.lastKeyInMidi != lastKeyInMidi) {
			Graphs.lastKeyInMidi = lastKeyInMidi;
			lastKey = Key.toKey(lastKeyInMidi);
			keysChanged();
		}
	}
	
	static final boolean isKeyboard() {
		return keyboard;
	}
	
	static final void setKeyboard(boolean keyboard) {
		if(Graphs.keyboard != keyboard) {
			Graphs.keyboard = keyboard;
			keyboardStateChanged();
		}
	}
	
	static final boolean isScoreImages() {
		return scoreImages;
	}
	
	static final void setScoreImages(boolean scoreImages) {
		if(Graphs.scoreImages != scoreImages) {
			Graphs.scoreImages = scoreImages;
			canvas.repaint();
		}
	}
	
	//-------------------- DEBUGGING FUNCTIONS --------------------
	
	/**
	 * Creates a string containing the most important information about the game. This method is
	 * used only for debugging and testing purposes.
	 * 
	 * @return the created string.
	 */
	static String getString() { //$SUP-MCP$
		return "Graphics()";
	} // end method
	
	/**
	 * This method serves security purposes. Provides an integrity string that will be checked by
	 * the {@link Connection#integrityCheck()} method; thus the application can only be altered if
	 * the source is known. Every class in the {@link keyboardhero} package has an integrity string.
	 * 
	 * @return the string of this class used for integrity checking.
	 */
	static String getIntegrityString() {
		return "Dďż˝>#jIďż˝h+Sl;H5'h.j_";
	} // end method
	
	/**
	 * The tester object of this class. It provides a debugging menu and unit tests for this class.
	 * Its only purpose is debugging or testing.
	 */
	final static Tester TESTER = new Tester("Graphics", new String[] {"getString()"} /* end initialization */) { //$SUP-PPA$
		void menu(int choice) throws Exception {
			switch(choice) {
				case 5:
					System.out.println(getString());
					break;
				default:
					baseMenu(choice);
					break;
			} // end switch
		} // end method
		
		void runUnitTests() throws Exception {
			higherTestStart("Graphics");
			
			testEq("getIntegrityString()", "Dďż˝>#jIďż˝h+Sl;H5'h.j_", Graphs.getIntegrityString());
			
			higherTestEnd();
		} // end method
	}; // end class
	
	/**
	 * Starts the class's developing menu. If this build is a developer's one it starts the
	 * application in a normal way with the exception that it starts the debugging tool for this
	 * class as well; otherwise exits with an error message.
	 * 
	 * @param args
	 *            the arguments given to the program.
	 * @see KeyboardHero#startApp()
	 */
	public static void main(String[] args) {
		Tester.mainer(args, TESTER);
	} // end method
} // end class
